CODv2 3.11 配列とポインタの対比
from CODv2 第3章「命令:マシンの言葉」
配列および配列インデックス?を使用したアセンブリ・コードと、ポインタを使用したアセンブリ・コードを比較する
アセンブリ・コードを比較することで、両者の違いが明確になる
メモリ中の「一連の語」をクリアする2つの手続きを C コードおよび MIPS アセンブリ・コードの両方で示す
配列を 0 クリアする手続きの C コード
概要
与えられるパラメータは2つ
語がストアされている配列 array
語の長さ(配列の大きさ) size
配列を使用する clear 手続きの C コード
code:exp3.11.1.c
clear1(int array[], int size)
{
int i;
for(i = 0; i < size; i = i + 1)
arrayi = 0;
}
ポインタを使用する clear 手続きの C コード
code:exp3.11.2.c
clear2(int *array, int size)
{
int *p;
for(p = &array0; p < &arraysize; p = P + 1)
*p = 0;
}
概要
引数 array は int へのポインタ型の変数
int *array という宣言
ローカル変数 p は int へのポインタ型の変数
int *p という宣言
&array[0] は配列 array[] の先頭の要素のアドレス
&array[size] は配列 array[] の最後の要素のアドレス
関節参照演算子( * 演算子)をつけて変数 p ( int へのポインタ型)の指す要素にアクセスする
*p = 0
変数 p の指す要素を 0 クリアする
配列を 0 クリアする手続きのアセンブリ・コード
前提
ループの本体に焦点をあてる
手続きの「つなぎ」に関するコードは無視することとする
レジスタの退避と復元
パラメータのコピーと引渡
など
配列を使用する clear 手続きのアセンブリ・コード
概要
2つのパラメータ array, size はそれぞれ引数レジスタ $a0, $a1 に割り付けられている
変数 i は一時レジスタ $t0 に割り付けられているものとする
for ループの変数 i の初期化
code:exp3.11.1.asm
move $t0, $zero # i = 0
i を4倍してバイト・アドレスを算出する(4バイト幅)
code:exp3.11.1.asm
loop1: add $t1, $t0, $t0 # $t1 = i * 2
add $t1, $t1, $t1 # $t1 = i * 4
array[i] のアドレスを算出
code:exp3.11.1.asm
add $t2, $a0, $t1 # $t2 = 'arrayi のアドレス'
for ループの本体
array[i] の要素を 0 クリアする
code:exp3.11.1.asm
sw $zero, 0($t2) # arrayi = 0
for ループの最後(ループの継続処理)
i を繰り上げる
code:exp3.11.1.asm
addi $t0, $t0, 1 # i = i + 1
for ループの終了判定
i と size を比較して、i < size でなければループから抜ける
$t0 < $a1
i < size であればループの先頭に戻る
code:exp3.11.1.asm
slt $t3, $t0, $a1 # ( i < size )なら $t3 = 1
bne $t3, $zero, loop1 # ( i < size )なら( $t3 != 0 なら) loop1 へ分岐
まとめると
code:exp3.11.1.all.asm
move $t0, $zero # i = 0
loop1: add $t1, $t0, $t0 # $t1 = i * 2
add $t1, $t1, $t1 # $t1 = i * 4
add $t2, $a0, $t1 # $t2 = arrayi のアドレス
sw $zero, 0($t2) # arrayi = 0
addi $t0, $t0, 1 # i = i + 1
slt $t3, $t0, $a1 # ( i < size )なら $t3 = 1
bne $t3, $zero, loop1 # ( i < size )なら( $t3 != 0 なら) loop1 へ分岐
条件判定がループの最後にある
CODv2 3.10 包括的な例題解説#68db922d00000000002ceb6d
この sort 手続きでは条件判定をループの最初で行っている
条件を満たさないときに、ループから抜ける条件判定をしているから
条件を満たさないときに exit1 へ分岐
どうしてループの最後で条件判定しているのか?
条件を満たしているときに、ループを継続する条件判定をしているから
条件を満たしているときに loop1 へ分岐
本文最後に但し書きがある
(このコードは size が 0 より大きい場合に正しく機能する)
ポインタを使用する clear 手続きのアセンブリ・コード
概要
2つのパラメータ array, size はそれぞれ引数レジスタ $a0, $a1 に割り付けられている
変数 p は一時レジスタ $t0 に割り付けられている
for ループの変数 p の初期化
code:exp3.11.2.asm
move $t0, $a0 # p = &array0
for ループの本体
p の指すメモリに 0 をストアする(要素を 0 クリアする)
code:exp3.11.2.asm
loop2: sw $zero, 0($t0) # メモリp = 0
for ループの継続処理
p を繰り上げる
p は int 型へのポインタ変数なので、これを1語分繰り上げるということは4バイト分、繰り上げるということ
code:exp3.11.2.asm
addi $t0, $t0, 4 # p = p + 4
for ループの終了判定
まず、配列 array の最後の要素の、先頭のアドレスを算出する
code:exp3.11.2.asm
add $t1, $a1, $a0 # $t1 = size * 2
add $t1, $t1, $t1 # $t1 = size * 4
array[size] のアドレスを算出する
array のベースアドレスに、array の最後の要素の、先頭のアドレスを加える
code:exp3.11.2.asm
add $t2, $a0, $t1 # $t2 = 'arraysize のアドレス'
p < 'array[size]のアドレス' であればループを継続する
code:exp3.11.2.asm
slt $t3, $t0, $t2 # p < 'arraysizeのアドレス' のとき $t3 = 1
bne $t3, $zero, loop2 # p < 'arraysizeのアドレス' なら loop2 へ分岐
まとめると
code:exp3.11.2.all.asm
move $t0, $a0 # p = 'array0 のアドレス'
loop2: sw $zero, 0($t0) # メモリp = 0
addi $t0, $t0, 4 # p = p + 4
add $t1, $a1, $a0 # $t1 = size * 2
add $t1, $t1, $t1 # $t1 = size * 4
add $t2, $a0, $t1 # $t2 = 'arraysize のアドレス'
slt $t3, $t0, $t2 # p < 'arraysizeのアドレス' のとき $t3 = 1
bne $t3, $zero, loop2 # p < 'arraysizeのアドレス' なら loop2 へ分岐
本文最後に但し書きがある
(このコードは size が 0 より大きい場合に正しく機能する)
この手続きではループのたびに 'array[size] のアドレス' を計算している。この計算はループの中で変更されない
そのためこの計算をループの外に出すと、実行速度が速くなる
code:exp3.11.2.all2.asm
move $t0, $a0 # p = 'array0 のアドレス'
add $t1, $a1, $a1 # $t1 = size * 2
add $t1, $t1, $t1 # $t1 = size * 4
add $t2, $a0, $t1 # $t2 = 'arraysize のアドレス'
loop2: sw $zero, 0($t0) # メモリp = 0
addi $t0, $t0, 4 # p = p + 4
slt $t3, $t0, $t2 # p < 'arraysizeのアドレス' のとき $t3 = 1
bne $t3, $zero, loop2 # p < 'arraysizeのアドレス' なら loop2 へ分岐
2つのバージョンの clear 手続きの比較
配列を使用する clear 手続きのアセンブリ・コード
配列インデックス版
CODv2 3.11 配列とポインタの対比#68f73c44000000000019bbde
code:exp3.11.1.all2.asm
move $t0, $zero # i = 0
loop1: add $t1, $t0, $t0 # $t1 = i * 2
add $t1, $t1, $t1 # $t1 = i * 4
add $t2, $a0, $t1 # $t2 = arrayi のアドレス
sw $zero, 0($t2) # arrayi = 0
addi $t0, $t0, 1 # i = i + 1
slt $t3, $t0, $a1 # ( i < size )なら $t3 = 1
bne $t3, $zero, loop1 # ( i < size )なら( $t3 != 0 なら) loop1 へ分岐
ポインタを使用する clear 手続きのアセンブリ・コード
ポインタ版
CODv2 3.11 配列とポインタの対比#68ff27b80000000000c2c70e
概要
配列インデックス版の手続きでは、ループ中でのアドレス計算のために乗算と加算を行わなければならない。ループ中で変数 i が繰り上げられ、それに基づいて毎回アドレスを計算し直す必要がある
ポインタ版の手続きでは、ポインタ p を直接繰り上げている(4バイト分)。その結果、1回のループ中で実行される命令数は、配列インデックス版では7ステップだが、ポインタ版では4ステップに削減されている
つまり配列インデックス版より、ポインタ版の方が実行速度が速い
ただし、最近のコンパイラでは、 clear1 のような C コード( CODv2 3.11 配列とポインタの対比#68f72367000000000019bb95 )を、 clear2 のようなアゼンブラ・コード( CODv2 3.11 配列とポインタの対比#68ff27b80000000000c2c70e )のように最適化するようになっている
補足
通常 C のコンパイラは変数 size がゼロよりも大きいことを確認するために、チェックを行う
その一つの方法は、 for ループ内の最初の命令の直前に slt 命令へのジャンプを追加すること
code:exp3.11.2.all2.asm
move $t0, $a0 # p = 'array0 のアドレス'
add $t1, $a1, $a1 # $t1 = size * 2
add $t1, $t1, $t1 # $t1 = size * 4
add $t2, $a0, $t1 # $t2 = 'arraysize のアドレス'
j check2 #
loop2: sw $zero, 0($t0) # メモリp = 0
addi $t0, $t0, 4 # p = p + 4
check2: slt $t3, $t0, $t2 # p < 'arraysizeのアドレス' のとき $t3 = 1
bne $t3, $zero, loop2 # p < 'arraysizeのアドレス' なら loop2 へ分岐
size が 0 であると( $a0 が 0 であると)、'array[size] のアドレス' は 'array[0] のアドレス' と等しくなる
このとき slt 命令で p >= 'array[size] のアドレス' なので $t3 = 0 なのでループ loop2 から抜ける